﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Reflection;
using nRoute.Components.Messaging;
using System.Xml;
using System.IO;
using nRoute.Internal;

namespace nRoute.Components.Mapping
{
    public static class AssemblyMapper
	{
		private static readonly List<Assembly> _mappedAssemblies;
        private static readonly Object _lock = new Object();

        static AssemblyMapper() 
        {
            _mappedAssemblies = new List<Assembly>();
            MapAssembly(typeof(AssemblyMapper).Assembly);
        }

#region Static Methods

        public static void GetAttributesInMappedAssembly<T>(Assembly assembly, Action<T, Type> mapAction)
            where 
                T : Attribute
        {
            Guard.ArgumentNotNull(assembly, "assembly");
            Guard.ArgumentNotNull(mapAction, "mapAction");

            // we get all the attributes type , note_ a type can have more than one instance 
            // of the given attribute applied..
            var _types = assembly.GetTypes();
            var _mappedAttributes = (from _type in _types
                                    let _attribs = _type.GetCustomAttributes(typeof(T), false).Cast<T>().ToList()
                                    where (_attribs != null && _attribs.Count > 0)
                                    select new MappedAttributes<T>()
                                    { 
                                        TargetType = _type, 
                                        TargetAttributes = _attribs 
                                    }).ToList();

            // we specifically add all the assembly-level attributes
            var _assemblyAttributes = assembly.GetCustomAttributes(typeof(T), false);
            if (_assemblyAttributes != null && _assemblyAttributes.Length > 0)
            {
                _mappedAttributes.Add(new MappedAttributes<T>()
                {
                    TargetType = assembly.GetType(),
                    TargetAttributes = _assemblyAttributes.Cast<T>().ToList()
                });
            }

            // we get all the applications of the a given attribute
          	foreach (var _mappedAttribute in _mappedAttributes)
			{
				// we need to check for each possible attribute applied 
				foreach (var _attribute in _mappedAttribute.TargetAttributes)
				{
				    mapAction(_attribute, _mappedAttribute.TargetType);	
				}				
			}			
		}

        public static void GetAttributesInMappedAssemblies<T>(Action<T, Type> mapAction)
            where 
                T : Attribute
		{
            Guard.ArgumentNotNull(mapAction, "mapAction");

            var _assemblies = MappedAssemblies;
            foreach (var _assembly in _assemblies)
            {
                GetAttributesInMappedAssembly<T>(_assembly, mapAction);
            }
		}

        public static void MapAssembly(Assembly assembly)
		{
            Guard.ArgumentNotNull(assembly, "assembly");

            lock (_lock)
            {
                if (!_mappedAssemblies.Contains(assembly))
                {
                    _mappedAssemblies.Add(assembly);
                    Channel.Publish<MappedAssemblyInfo>(new MappedAssemblyInfo(assembly));
                }
            }
		}

        public static void MapApplicationAssemblies()
		{
            lock (_lock)
            {
                // we get all the included assemblies
                var _applicationAssembly = GetManifestIncludedAssemblies();

                // and map all listed assemblies - note_ the mapping is actually using an observable channel
                foreach (var _assembly in _applicationAssembly)
                {
                    if (!_mappedAssemblies.Contains(_assembly))
                    {
                        _mappedAssemblies.Add(_assembly);
                        Channel.Publish<MappedAssemblyInfo>(new MappedAssemblyInfo(_assembly));
                    }
                }
            }
		}

        public static bool IsAssemblyMapped(Assembly assembly)
		{
            lock (_lock)
            {
                // note_ in SL, we can't unload an assembly once loaded
                return _mappedAssemblies.Contains(assembly);
            }
		}

        public static IEnumerable<Assembly> MappedAssemblies 
		{
			get
			{
                Assembly[] _assemblies = null;
                lock (_lock)
                {
                    _assemblies = _mappedAssemblies.ToArray();
                }
                return _assemblies;
			}
		}

#endregion

#region Helpers

#if (!WP7)

        public static IEnumerable<Assembly> GetManifestIncludedAssemblies()
        {

#if !SILVERLIGHT

            return AppDomain.CurrentDomain.GetAssemblies();

#else

#if DEBUG 
            if (DesignerProperties.IsInDesignTool)
            {
                // tip from http://joshsmithonwpf.wordpress.com/2010/01/06/the-ultimate-hack-for-silverlight-in-blend/
                return typeof (AppDomain).GetMethod("GetAssemblies").Invoke(AppDomain.CurrentDomain, null) as Assembly[];
            }
#endif

            // ideas from http://pagebrooks.com/archive/2009/05/17/a-smaller-xap-preloader-for-silverlight.aspx
            // load from app manifest the zip stream
            var _applicaAssemblies = new List<Assembly>();
            var _manifestStream = Application.GetResourceStream(new Uri("AppManifest.xaml", UriKind.Relative));

            // get the xml reader
            string _manifest = new StreamReader(_manifestStream.Stream).ReadToEnd();
            XmlReader _reader = XmlReader.Create(new StringReader(_manifest));

            // read all the AssemblyPart elements
            while (_reader.Read())
            {
                if (_reader.IsStartElement("AssemblyPart"))
                {
                    // we get the source attribute which has the dll file name
                    _reader.MoveToAttribute("Source");
                    _reader.ReadAttributeValue();

                    // get the name of the assembly, and check if it has been 
                    var _assemblyName = _reader.Value;
                    var _isAlreadyLoaded = _applicaAssemblies.FirstOrDefault((a) =>
                        string.Equals(a.ManifestModule.ToString(), _assemblyName, 
                            StringComparison.InvariantCultureIgnoreCase)) != null;

                    // if not already loaded
                    if (!_isAlreadyLoaded)
                    {
#if SILVERLIGHT
                        // we get the assembly
                        Assembly _assembly = null;
                        var _stream = Application.GetResourceStream(
                            new Uri(_assemblyName, UriKind.Relative));

                        // create a assembly part to load
                        AssemblyPart _assemblyPart = new AssemblyPart();
                        _assembly = _assemblyPart.Load(_stream.Stream);
#else
                        // we get the assembly
                        Assembly _assembly = Assembly.Load(_assemblyName);
#endif

                        // and add to the list
                        _applicaAssemblies.Add(_assembly);
                    }
                }
            }

            // and return
            return _applicaAssemblies;

#endif

        }

#else

        public static IEnumerable<Assembly> GetManifestIncludedAssemblies()
        {
            // This is a technique from SL2 - and it should work coz all the assemblies should be inter-used in someways
            // if not then one might have to call load assembly individually
            var _loadedAssemblies = new List<Assembly>();
            var _assemblyLister = null as Action<Assembly, List<Assembly>>;

            // recurusive lister
            _assemblyLister = (a, l) =>
            {
                if (_loadedAssemblies.Contains(a)) return;
                _loadedAssemblies.Add(a);
                foreach (var m in a.GetModules())
                {
                    if (!_loadedAssemblies.Contains(m.Assembly))
                        _assemblyLister(m.Assembly, _loadedAssemblies);
                }
            };

            // and we load starting with the three assembly references we known off
            _assemblyLister(Assembly.GetCallingAssembly(), _loadedAssemblies);
            _assemblyLister(Assembly.GetExecutingAssembly(), _loadedAssemblies);
            _assemblyLister(System.Windows.Application.Current.GetType().Assembly, _loadedAssemblies);

            return _loadedAssemblies;
        }

#endif

#endregion

#region Private Class

        private class MappedAttributes<T>
            where 
                T : Attribute
        {
            public Type TargetType { get; set; }

            public IEnumerable<T> TargetAttributes { get; set; }
        }

#endregion

    }
}